JWT + Redis 双重会话管理 学习笔记

前言

本笔记基于对微服务架构中会话管理方案的学习与思考,重点探讨了 JWT + Redis 双重验证机制 的设计原理、架构权衡以及鉴权扩展方案。

一、核心设计:JWT + Redis 双重会话管理

1.1 设计目标

这个设计旨在解决 纯 JWT纯 Session 各自的痛点:

方案 优点 缺点
纯 JWT 无状态、易于横向扩展 无法主动注销、Token 较长
纯 Session 可随时注销、可存储丰富信息 需要维护服务端状态、分布式困难
JWT + Redis 兼顾两者优点 复杂度稍高

1.2 代码实现

// ==================== 登录时:同时生成 JWT 和存储 Redis ====================

// 1. 存入 Redis(存储用户完整信息)
redisCache.set(
    RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, user.getId()),
    user,
    tokenExpireTime,
    TimeUnit.MINUTES
);

// 2. 生成 JWT(只包含 userId 等基本信息)
userLoginVo.setToken(createToken(user.getId(), getChannelDataByCode(code).getTokenSecret()));
// ==================== 网关验证时:两者都要检查 ====================

public UserVo getUser(String token, String code, String tokenSecret) {
    // 第一重:解析 JWT 获取 userId(验证签名完整性)
    String userId = parseToken(token, tokenSecret);

    // 第二重:检查 Redis 是否存在登录态
    if (StringUtil.isNotEmpty(userId)) {
        userVo = redisCache.get(
            RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, userId),
            UserVo.class
        );
    }

    // 两重验证都通过才放行
    return Optional.ofNullable(userVo)
        .orElseThrow(() -> new DaMaiFrameException(BaseCode.LOGIN_USER_NOT_EXIST));
}

二、核心问题探讨

2.1 我的疑问

我的理解是:用 Token 而不是 Session 其实就是为了无状态*,无需在服务端维护连接信息,节省空间。但缺点是只能解析出 id,一些信息得通过 id 查表才能拿到,这些可能会损耗一定资源。而 Session 可以存储一些信息在登录态里面,登录成功后无需查数据库。*

但是这里把用户信息存在 Redis 里,岂不是又花了维护连接的空间,还花了查询的资源和时间,仅仅只是为了能踢人?那为什么不直接用 Session + Redis 呢?

2.2 架构权衡分析

这个质疑确实直击架构本质。在微服务高并发的架构背景下,选择 JWT + Redis 而不是 Session + Redis,基于以下 4 个核心维度的考量:

维度一:微服务架构下的"透传"优势(核心差异)

Session + Redis 方案的问题:

用户请求 → 网关(查Redis) → 订单服务(再查Redis) → 票务服务(再查Redis)
                ↓                    ↓                    ↓
           Redis压力 ×1          Redis压力 ×2          Redis压力 ×3

JWT + Redis 方案(本方案):

用户请求 → 网关(查Redis验证,解析JWT) → 订单服务(直接用JWT信息) → 票务服务(直接用JWT信息)
                ↓                              ↓                          ↓
           Redis压力 ×1                   无需查Redis                  无需查Redis

💡 核心收益:网关负责"脏活累活",下游服务通过 CPU 计算验证 JWT 签名即可,极大减少了 Redis 的 QPS 压力,实现了"一次认证,处处可用"。

维度二:多端兼容性

方案 Web 浏览器 iOS/Android App 小程序 第三方接口
Session/Cookie ✅ 原生支持 ⚠️ 需要 CookieJar ⚠️ 跨域问题 ❌ 不友好
JWT (Header) ✅ 支持 ✅ 标准 HTTP ✅ 标准 HTTP ✅ 标准 HTTP

💡 如果项目业务 App 端流量很大,Token 形式远优于 Cookie/Session 形式

维度三:"柔性鉴权"与降级能力

代码中有 skipCheckTokencheckNeedUserId 逻辑:

boolean skipCheckTokenResult = skipCheckToken(url);
// ...
if (!skipCheckTokenResult) {
    // 强制查 Redis
    UserVo userVo = tokenService.getUser(...);
}

高并发降级场景(如 Redis 响应变慢):

方案 降级能力
Session 方案 ❌ 系统直接不可用
JWT + Redis ✅ 非核心业务可只校验 JWT 签名,跳过 Redis 检查

💡 虽然降级时无法"踢人",但至少保证了业务不崩,用户能看能刷

维度四:解决纯 JWT 的"无法撤销"问题

对于涉及资金交易(买票)的系统,这是安全刚需:

场景 纯 JWT JWT + Redis
用户手机丢失 ❌ 无法强制下线 ✅ 删除 Redis Key 即可
封禁黄牛账号 ❌ Token 未过期仍有效 ✅ 删除 Redis Key 即可
用户主动退出 ❌ Token 仍然有效 ✅ 删除 Redis Key 即可

💡 Redis 的存储成本,是为"安全可控"支付的保险费。

2.3 方案对比总结

维度 纯 Session + Redis 纯 JWT JWT + Redis (本方案)
存储压力 高 (Redis) 中/高 (Redis)
带宽压力 低 (SessionID 短) 高 (Token 长) 高 (Token 长)
多端兼容 差 (依赖 Cookie) 好 (Header) 好 (Header)
微服务传递 难 (需共享 Redis) 易 (自带信息) 易 (网关查完后下游自带信息)
踢人/注销 ✅ 支持 ❌ 不支持 ✅ 支持
降级能力 ❌ 无 (必须查库) ✅ 强 (只验签) ✅ 强 (可灵活配置)

2.4 结论

📌 如果是单体 Web 应用,Session + Redis 确实是更简单的选择。

📌 但因为这是微服务 + 多端 App + 高并发抢票场景,"微服务间的信息透传" 和 "多端兼容" 的权重压倒了 "节省 Redis 空间和 Token 流量" 的成本。

📌 这是一种用 "Redis 空间"和"Token 流量" 换取 "架构灵活性"和"服务解耦" 的权衡设计。

三、鉴权逻辑分析

3.1 我的疑问

这套系统里是不是没有严格的鉴权逻辑?之前的单体项目是使用 RBAC 模型,借助 Redis 存放 Session 和 LoginVo(里面包含了用户的身份集合和权限集合),每次请求打到就 AOP 拦截先校验,通过注解的形式打在某个接口上实现接口级别的精细鉴权。

这里 Redis 里也存了 UserVo,貌似可以扩展到那种形式?

3.2 To C 与 To B 的鉴权差异

To B(管理后台)—— 功能权限

场景:内部系统、ERP、CMS
特点:用户分三六九等(超级管理员、财务、运营、普通员工)
核心:功能权限(Functional Authority)
实现:@PreAuthorize("hasRole('ADMIN')"),基于 RBAC 模型

To C(消费者端)—— 数据权限

场景:数百万普通用户来抢票
特点:99.9% 的用户角色都是"普通会员"
核心:数据权限(Data Authority)
实现:代码里的逻辑校验(currentUserId == order.userId)

💡 这套系统目前展示的是 C 端逻辑,所以没有看到复杂的 RBAC。但在后台管理模块中,RBAC 方案是标准答案。

3.3 如何扩展 RBAC 鉴权

Step 1:扩充 UserVo

public class UserVo {
    private Long id;
    private String username;

    // 扩展:角色集合
    private Set<String> roles;

    // 扩展:权限标识集合 (e.g., "program:add", "order:export")
    private Set<String> permissions;
}

Step 2:登录时注入权限

// 管理员登录成功后
UserVo userVo = new UserVo();
userVo.setId(user.getId());
userVo.setUsername(user.getUsername());

// 查询角色和权限
userVo.setRoles(roleService.getRolesByUserId(user.getId()));
userVo.setPermissions(permissionService.getPermissionsByUserId(user.getId()));

// 存入 Redis
redisCache.set(key, userVo, tokenExpireTime, TimeUnit.MINUTES);

Step 3:鉴权拦截位置选择

位置 适用场景 优点 缺点
网关层鉴权 粗粒度拦截 无权限请求进不去微服务,节省资源 需维护 URL-权限映射,配置繁琐
服务内 AOP 细粒度拦截 开发体验好,权限跟着接口走 请求已进入服务内部

推荐组合

网关:全局黑白名单 + 登录态检查
服务内 AOP:业务强相关的细粒度拦截(如 @RequiresPermissions("user:delete"))

3.4 微服务下的特殊挑战

挑战一:内存膨胀问题

场景 用户量 Redis 存储策略
单体 To B 几千内部员工 完整权限列表,无压力
微服务 To C 1000万活跃用户 ⚠️ 每人存权限列表会爆内存

优化方案

  • C 端用户:Redis 只存基本信息,不存权限
  • B 端/VIP 用户:才存储权限列表
  • 使用 BitMap角色 ID 代替具体的权限字符串列表

挑战二:权限变更的实时性

问题场景:

1. 在数据库里删除了管理员的权限
2. 但他的 UserVo 还在 Redis 里(TTL 30分钟)
3. 结果:他依然拥有权限,直到 Redis 过期

解决方案:

// 修改权限时,主动删除/更新该用户在 Redis 中的 UserVo 缓存
public void updateUserPermissions(Long userId, Set<String> newPermissions) {
    // 1. 更新数据库
    permissionMapper.updateByUserId(userId, newPermissions);
    
    // 2. 删除 Redis 缓存(强制下次请求重新加载)
    redisCache.delete(RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, userId));
}

四、核心理解总结

4.1 会话管理的本质

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   Token(JWT)= 钥匙(Key)                                  │
│   - 只是一个凭证,用于证明"我是谁"                            │
│   - 存储在客户端                                             │
│                                                             │
│   Redis = 锁芯(Session Store)                              │
│   - 真正的状态存储,包含用户详细信息                          │
│   - 存储在服务端,可随时控制(踢人、封禁)                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4.2 双重验证流程

        客户端请求
            │
            ▼
    ┌───────────────┐
    │ 携带 JWT Token │
    └───────┬───────┘
            │
            ▼
    ┌───────────────────────┐
    │ 第一重:验证 JWT 签名    │  ← CPU 计算,验证 Token 未被篡改
    │ (解析出 userId)       │
    └───────────┬───────────┘
                │
                ▼
    ┌───────────────────────┐
    │ 第二重:查询 Redis      │  ← IO 操作,验证登录态是否存在
    │ (检查是否被踢/过期)    │
    └───────────┬───────────┘
                │
        ┌───────┴───────┐
        │               │
        ▼               ▼
    ✅ 放行          ❌ 拒绝

4.3 鉴权层次

层次 关注点 实现方式
认证(Authentication) 你是谁? JWT 解析 + Redis 状态检查
授权(Authorization) 你能做什么? RBAC + AOP 注解
数据权限(Data Scope) 你能看哪些数据? 业务代码逻辑校验

五、设计启示

5.1 架构选型不是非黑即白

没有"最好"的方案,只有"最适合"的方案。JWT + Redis 看似"两者缺点都占了",但在特定场景下却是最优解。

5.2 理解场景是关键

单体应用 + Web 端     →  Session + Redis 就够了
微服务 + 多端 + 高并发  →  JWT + Redis 更合适

5.3 安全与性能的权衡

Redis 存储成本 = 安全可控的保险费

在涉及资金交易的系统中,"能踢人"不是可选项,而是刚需。


企业级项目导航:⬅️ 04-(缓存预热)购票人业务架构分析 | 05-JWT + Redis 双重会话管理 学习笔记 | ➡️ 06-Lua 深度学习笔记